package com.nts.visionworks.business.endpoint.service;

import java.util.Date;
import java.util.List;
import java.util.Set;

import javax.annotation.Resource;

import org.apache.commons.lang.StringUtils;
import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Property;
import org.hibernate.criterion.Restrictions;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.nts.visionworks.business.endpoint.domain.Building;
import com.nts.visionworks.business.endpoint.domain.BuildingFloor;
import com.nts.visionworks.business.endpoint.domain.BuildingFloorArea;
import com.nts.visionworks.business.endpoint.domain.BuildingFloorImage;
import com.nts.visionworks.business.endpoint.domain.EndPoint;
import com.nts.visionworks.business.endpoint.domain.EndPointCategory;
import com.nts.visionworks.business.endpoint.domain.EndPointScreenshot;
import com.nts.visionworks.business.endpoint.domain.EndPointScreenshotId;
import com.nts.visionworks.business.endpoint.domain.ErrorRecord;
import com.nts.visionworks.business.endpoint.domain.ErrorRecordId;
import com.nts.visionworks.business.endpoint.domain.IpType;
import com.nts.visionworks.business.endpoint.domain.PowerStatus;
import com.nts.visionworks.core.hibernate.transform.AutoCastableResultTransformer;

/**
 * 끝점과 위치 정보(건물)에 대한 DAO를 제공한다.
 * @author Devcken
 */
@Service("endPointService")
@Transactional
public class EndPointService
{
	/**
	 * VisionWorks {@link org.hibernate.SessionFactory}를 주입받는다.
	 */
	@Resource(name="visionworksSessionFactory")
	private SessionFactory _sessionFactory;
	
	/**
	 * 끝점 목록을 조회한다.
	 * @param searchCondition 끝점 목록 조회를 위한 검색 조건을 나타내는 {@link com.nts.visionworks.business.endpoint.domain.EndPoint} 인스턴스를 설정한다.
	 * @param areaId 특정 층 구역에 속하는 끝점 목록을 조회하기 위한 층 구역 아이디를 설정한다.
	 * @return 조회된 끝점 목록이 반환된다. 
	 */
	@SuppressWarnings("unchecked")
	public List<EndPoint> listEndPoints(EndPoint searchCondition, int areaId)
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			Criteria criteria = session.createCriteria(EndPoint.class, "ep");
			
			// 층 구역 아이디가 설정된 경우, BuildingFloorArea 테이블과 조인을 맺게 한다.
			// 만약 설정되지 않는 경우, 지역 정보를 설정하지 않고 조회한다.
			if (areaId > 0)
			{
				criteria.createAlias("buildingFloorArea", "fa");
				criteria.add(Restrictions.eqProperty("fa.areaId", String.valueOf(areaId)));
			}
			
			// IP 주소에 대한 LIKE 검색 조건을 설정한다.
			if (!StringUtils.isEmpty(searchCondition.getIpAddress()))
			{
				criteria.add(Restrictions.like("ep.ipAddress", searchCondition.getIpAddress()));
			}
			
			// 물리 주소에 대한 LIKE 검색 조건을 설정한다.
			if (!StringUtils.isEmpty(searchCondition.getMacAddress()))
			{
				criteria.add(Restrictions.like("ep.macAddress", searchCondition.getMacAddress()));
			}
			
			// 전원 상태에 대한 EQUAL 검색 조건을 설정한다.
			if (searchCondition.getPowerStatus() > 0)
			{
				criteria.add(Restrictions.eq("ep.powerStatus", searchCondition));
			}
			
			// 사용 가능 여부에 대한 EQUAL 검색 조건을 설정한다.
			criteria.add(Restrictions.eq("ep.usable", searchCondition.isUsable()));
		
			List<EndPoint> identities = criteria.list();
			
			return identities;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 끝점 정보를 새로 추가한다.
	 * 끝점을 새로 추가할 때는 끝점의 위치 정보, 이름, IP 유형, IP 주소만을 등록한다.
	 * @param areaId 끝점을 위치시킬 층 구역 아이디를 설정한다.
	 * @param endPointName 끝점 이름을 설정한다.
	 * @param ipType IP 유형을 설정한다.
	 * @param ipAddress IP 주소를 설정한다.
	 * @return 새로 추가된 끝점 정보가 반환된다.
	 */
	public EndPoint insertEndPoint(int areaId, String endPointName, IpType ipType, String ipAddress)
	{
		Session session = this._sessionFactory.openSession();
		
		Transaction transaction = session.beginTransaction();
		
		try
		{
			Criteria areaCriateria = session.createCriteria(BuildingFloorArea.class)
					.add(Restrictions.eq("areaId", areaId));
			
			BuildingFloorArea area = (BuildingFloorArea)areaCriateria.uniqueResult();
			
			EndPoint endPoint = new EndPoint();
			
			endPoint.setBuildingFloorArea(area);
			endPoint.setEndPointName(endPointName);
			endPoint.setIpType(ipType.value);
			endPoint.setIpAddress(ipAddress);
			
			session.save(endPoint);
			
			transaction.commit();
			
			return endPoint;
		}
		catch (Exception error)
		{
			transaction.rollback();
			
			throw error;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 끝점 정보를 수정한다.
	 * @param endPoint 수정할 끝점 정보를 설정한다.
	 * @return 수정된 끝점 정보가 반환된다.
	 */
	public EndPoint updateEndPoint(EndPoint endPoint)
	{
		Session session = this._sessionFactory.openSession();
		
		Transaction transaction = session.beginTransaction();
		
		try
		{
			Criteria endPointCriteria = session.createCriteria(EndPoint.class)
					.add(Restrictions.eq("endPointId", endPoint.getEndPointId()));
			
			// 주어진 끝점 아이디에 대해서 현재 데이터베이스에 등록되어 있는 끝점 정보를 열람한다.
			EndPoint currentEndPoint = (EndPoint)endPointCriteria.uniqueResult();
		
			// 주어진 끝점 아이디에 대한 끝점 정보가 존재하지 않을 경우, 더 이상 진행할 수 없다.
			if (currentEndPoint == null)
			{
				return null;
			}
			
			// IP가 변경된 경우, 물리 주소와 전원 상태를 초기화한다.
			if (endPoint.getIpAddress().equals(currentEndPoint.getIpAddress()))
			{
				endPoint.setMacAddress(null);
				endPoint.setPowerStatus(PowerStatus.PowerOff.value);
			}
			
			session.update(endPoint);
			
			transaction.commit();
			
			return endPoint;
		}
		catch (Exception error)
		{
			transaction.rollback();
			
			throw error;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 끝점을 사용 불가능하도록 수정한다.
	 * @param endPointsId 사용 불가능하도록 수정하려는 끝점 아이디 목록을 설정한다.
	 * @return 사용 불가능하도록 수정된 끝점의 개수가 반환된다.
	 */
	public int disableEndPoints(List<Long> endPointsId)
	{
		Session session = this._sessionFactory.openSession();
		
		Transaction transaction = session.beginTransaction();
		
		try
		{
			// 끝점 아이디 목록이 null이거나 크기가 0이면 실패로 처리한다.
			if (endPointsId == null || endPointsId.size() == 0)
			{
				return 0;
			}
			
			Query query = session.getNamedQuery("EndPoint.UpdateUsable").setParameterList("endPointsId", endPointsId);
			
			int resultCount = query.executeUpdate();
			
			transaction.commit();
			
			return resultCount;
		}
		catch (Exception error)
		{
			transaction.rollback();
			
			throw error;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 주어진 IP 주소가 끝점 목록 중에 존재하는지 판단한다.
	 * @param ipAddress 존재하는지 알아보고자 하는 IP 주소를 설정한다.
	 * @return 주어진 IP 주소가 끝점 목록 중에 존재하지 않으면 true, 존재하면 false가 반환된다.
	 */
	public boolean containsIpAddress(String ipAddress)
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			Criteria endPointCriteria = session.createCriteria(EndPoint.class)
					.add(Restrictions.eq("ipAddress", ipAddress))
					.setProjection(Projections.rowCount());
			
			int resultCount = (int)endPointCriteria.uniqueResult();
			
			return resultCount == 0;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 주어진 물리 주소가 끝점 목록 중에 존재하는지 판단한다.
	 * @param macAddress 존재하는지 알아보고자 하는 물리 주소를 설정한다.
	 * @return 주어진 물리 주소가 끝점 목록 중에 존재하지 않으면 true, 존재하면 false가 반환된다.
	 */
	public boolean containsMacAddress(String macAddress)
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			Criteria endPointCriteria = session.createCriteria(EndPoint.class)
					.add(Restrictions.eq("macAddress", macAddress))
					.setProjection(Projections.rowCount());
			
			int resultCount = (int)endPointCriteria.uniqueResult();
			
			return resultCount == 0;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 끝점 카테고리를 조회한다.
	 * @param startLikeHierachicalLabel 계층 라벨에 대해 검색하기 위해 검색 파라메터를 설정한다.
	 * @return 조회된 끝점 카테고리를 반환한다.
	 */
	@SuppressWarnings("unchecked")
	public List<EndPointCategory> listEndPointCategories(String startLikeHierachicalLabel)
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			Query query = session.getNamedQuery("EndPoint.ListEndPointCategoryHierachically")
					.setString("StartLikeHierachicalLabel", startLikeHierachicalLabel)
					.setResultTransformer(new AutoCastableResultTransformer<EndPointCategory>(EndPointCategory.class));
			
			return query.list();
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 끝점 카테고리를 조회한다.
	 * @return 조회된 끝점 카테고리를 반환한다.
	 */
	public List<EndPointCategory> listEndPointCategories()
	{
		return this.listEndPointCategories("");
	}
	
	/**
	 * 끝점 카테고리를 저장한다.
	 * @param category 저장할 끝점 카테고리 정보를 설정한다.
	 */
	public void saveEndPointCategory(EndPointCategory category)
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			session.saveOrUpdate(category);
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 끝점 카테고리를 삭제한다.
	 * @param categoryId 삭제하려는 끝점 카테고리 아이디를 설정한다.
	 */
	public void deleteEndPointCategory(long categoryId)
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			Query query = session.getNamedQuery("EndPoint.DeleteEndPointCategory")
					.setLong("categoryId", categoryId);
			
			query.executeUpdate();
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 주어진 조건에 맞는 끝점의 스크린샷 목록을 조회한다.
	 * @param powerStatus 끝점의 전원 상태 검색 조건을 설정한다.
	 * @param areaId 끝점의 위치 검색 조건을 설정한다.
	 * @param endPointId 끝점 스크린샷에 대한 끝점 아이디를 설정한다.
	 * @return 주어진 조건에 맞는 끝점의 스크린샷 목록이 반환된다.
	 */
	@SuppressWarnings("unchecked")
	public List<EndPointScreenshot> listEndPointScreenshots(PowerStatus powerStatus, int areaId, long endPointId)
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			DetachedCriteria endPointScreenshotSubCriteria = DetachedCriteria.forClass(EndPointScreenshot.class)
					.setProjection(Projections.distinct(Projections.id()))
					.setProjection(Projections.groupProperty("endPoint.endPointId"))
					.addOrder(Order.asc("id.uploadedDateTime"));
			
			Criteria endPointCriteria = session.createCriteria(EndPoint.class, "ep")
					.createAlias("endPointScreenshots", "es")
					.add(Property.forName("es.id.uploadedDateTime").eq(endPointScreenshotSubCriteria));
			
			if (powerStatus != PowerStatus.Unknown)
			{
				endPointCriteria.add(Restrictions.eq("ep.powerStatus", powerStatus.value));
			}
			
			if (areaId > 0)
			{
				endPointCriteria.add(Restrictions.eq("ep.areaId", areaId));
			}
			
			if (endPointId > 0)
			{
				endPointCriteria.add(Restrictions.eq("ep.endPointId", endPointId));
			}
			
			endPointCriteria.add(Restrictions.eq("ep.usable", true));
			
			return endPointCriteria.list();
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 주어진 끝점의 스크린샷 목록을 조회한다.
	 * @param endPointId 스크린샷을 조회하려는 끝점의 아이디를 설정한다.
	 * @return 주어진 끝점의 스크린샷 목록이 반환된다.
	 */
	@SuppressWarnings("unchecked")
	public List<EndPointScreenshot> listEndPointScreenshots(long endPointId)
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			Criteria endPointCriteria = session.createCriteria(EndPointScreenshot.class)
					.add(Restrictions.eq("endPointId", endPointId));
			
			return endPointCriteria.list();
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 끝점 스크린샷 정보를 저장한다.
	 * @param endPointId 저장하려는 스크린샷의 끝점 아이디를 설정한다.
	 * @param screenshotArchiveId 스크린샷의 아카이브 아이디를 설정한다.
	 * @return 끝점 스크린샷 정보가 저장되면 true, 그렇지 않으면 false가 반환된다.
	 */
	public boolean saveEndPointScreenshot(long endPointId, long screenshotArchiveId)
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			Criteria endPointCriteria = session.createCriteria(EndPoint.class)
					.add(Restrictions.eq("endPointId", endPointId));
			
			EndPoint endPoint = (EndPoint)endPointCriteria.uniqueResult();
			
			// 주어진 끝점 아이디에 대한 끝점 정보가 존재하지 않으면 실패로 처리한다.
			if (endPoint == null)
			{
				return false;
			}
			
			EndPointScreenshot screenshot = new EndPointScreenshot(new EndPointScreenshotId(endPointId, new Date()), endPoint, screenshotArchiveId);
			
			session.save(screenshot);
			
			return true;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}

	/**
	 * 보관 기간이 만료된 끝점 스크린샷들을 정리한다.
	 */
	public void cleanExpiredEndPointScreenshots()
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			Query query = session.getNamedQuery("EndPoint.CleanExpiredEndPointScreenshots");
			
			query.executeUpdate();
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 주어진 끝점 아이디와 업로드된 날짜/시각에 해당하는 스크린샷을 삭제한다.
	 * @param endPointId 삭제하고자 하는 스크린샷의 대상 끝점 아이디를 설정한다.
	 * @param uploadedDateTime 스크린샷이 업로드된 날짜/시각을 설정한다.
	 * @return 끝점 스크린샷이 삭제되었으면 true, 그렇지 않으면 false가 반환된다. 
	 */
	public boolean deleteEndPointScreenshot(long endPointId, Date uploadedDateTime)
	{
		Session session = this._sessionFactory.openSession();
		
		Transaction transaction = session.beginTransaction();
		
		try
		{
			Criteria screenshotCriteria = session.createCriteria(EndPointScreenshot.class)
					.add(Restrictions.eq("endPointId", endPointId))
					.add(Restrictions.eq("uploadedDateTime", uploadedDateTime));
			
			EndPointScreenshot screenshot = (EndPointScreenshot)screenshotCriteria.uniqueResult();
			
			if (screenshot == null) return false;
			
			session.delete(screenshot);
			
			return true;
		}
		catch (Exception error)
		{
			transaction.rollback();
			
			throw error;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 빌딩 목록을 조회한다.
	 * @return 조회된 빌딩 목록이 반환된다.
	 */
	@SuppressWarnings("unchecked")
	public List<Building> listBuildings()
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			Criteria buildingCriteria = session.createCriteria(Building.class)
					.addOrder(Order.asc("buildingName"));
			
			List<Building> buildings = buildingCriteria.list();
			
			for (Building building : buildings)
			{
				Set<BuildingFloor> floors = building.getBuildingFloors();
				
				for (BuildingFloor floor : floors)
				{
					Hibernate.initialize(floor.getBuildingFloorAreas());
					Hibernate.initialize(floor.getBuildingFloorImages());
				}
			}
			
			return buildings;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 빌딩 정보를 저장한다.
	 * @param building 저장할 빌딩 정보를 설정한다.
	 * @return 빌딩 정보가 저장되었으면 true, 그렇지 않으면 false가 반환된다.
	 */
	public boolean saveBuilding(Building building)
	{
		Session session = this._sessionFactory.openSession();
		
		Transaction transaction = session.beginTransaction();
		
		try
		{
			building.setCreatedDateTime(new Date());
			
			session.save(building);
			
			transaction.commit();
			
			return true;
		}
		catch (Exception error)
		{
			transaction.rollback();
			
			throw error;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 빌딩 층 정보를 저장한다.
	 * @param floor 저장할 빌딩 층 정보를 설정한다.
	 * @return 빌딩 층 정보가 저장되었으면 true, 그렇지 않으면 false가 반환된다.
	 */
	public boolean saveBuildingFloor(BuildingFloor floor, Integer buildingId)
	{
		Session session = this._sessionFactory.openSession();
		
		Transaction transaction = session.beginTransaction();
		
		try
		{
			Criteria buildingCriteria = session.createCriteria(Building.class)
					.add(Restrictions.eq("buildingId", buildingId));
			
			Building building = (Building)buildingCriteria.uniqueResult();
			
			floor.setBuilding(building);
			
			session.save(floor);
			
			transaction.commit();
			
			return true;
		}
		catch (Exception error)
		{
			transaction.rollback();
			
			throw error;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 빌딩 층 구역 정보를 저장한다.
	 * @param area 저장할 빌딩 층 구역 정보를 설정한다.
	 * @param floorId 빌딩 층 아이디를 설정한다.
	 * @return 빌딩 층 구역 정보가 저장되었으면 true,  그렇지 않으면 false가 반환된다.
	 */
	public boolean saveBuildingFloorArea(BuildingFloorArea area, int floorId)
	{
		Session session = this._sessionFactory.openSession();
		
		Transaction transaction = session.beginTransaction();
		
		try
		{
			Criteria floorCriteria = session.createCriteria(BuildingFloor.class)
					.add(Restrictions.eq("floorId", floorId));
			
			BuildingFloor floor = (BuildingFloor)floorCriteria.uniqueResult();
			
			area.setBuildingFloor(floor);
			
			session.save(area);
			
			transaction.commit();
			
			return true;
		}
		catch (Exception error)
		{
			transaction.rollback();
			
			throw error;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 주어진 빌딩 층 아이디의 이미지 목록을 조회한다.
	 * @param floorId 빌딩 층 아이디를 설정한다.
	 * @return 주어진 빌딩 층 이미지 목록이 반환된다.
	 */
	@SuppressWarnings("unchecked")
	public List<BuildingFloorImage> listBuildingFloorImages(int floorId)
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			Criteria floorImageCriteria = session.createCriteria(BuildingFloorImage.class)
					.add(Restrictions.eq("id.floorId", floorId));
			
			return floorImageCriteria.list();
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 끝점 오류 기록 목록을 조회한다.
	 * @param endPointId 조회하려는 오류 기록에 대한 끝점 아이디를 설정한다.
	 * @return 조회된 오류 기록 목록이 반환된다.
	 */
	@SuppressWarnings("unchecked")
	public List<ErrorRecord> listErrorRecords(long endPointId)
	{
		Session session = this._sessionFactory.openSession();
		
		try
		{
			Criteria errorRecordsCriteria = session.createCriteria(ErrorRecord.class);
			
			if (endPointId > 0)
			{
				errorRecordsCriteria.add(Restrictions.eq("id.endPointId", endPointId));
			}
			
			return errorRecordsCriteria.list();
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
	
	/**
	 * 끝점 오류 기록을 저장한다.
	 * @param endPointId 저장하려는 오류 기록에 대한 끝점 아이디를 설정한다.
	 * @param errorRecord 저장하려는 오류 기록을 설정한다.
	 * @return 오류 기록이 저장되면 true, 그렇지 않으면 false가 반환된다.
	 */
	public boolean saveErrorRecord(long endPointId, ErrorRecord errorRecord)
	{
		Session session = this._sessionFactory.openSession();
		
		Transaction transaction = session.beginTransaction();
		
		try
		{
			errorRecord.setId(new ErrorRecordId(new Date(), endPointId));
			
			session.save(errorRecord);
			
			transaction.commit();
			
			return true;
		}
		catch (Exception error)
		{
			transaction.rollback();
			
			throw error;
		}
		finally
		{
			if (session != null && session.isOpen())
			{
				session.close();
			}
		}
	}
}